import { Callout } from 'nextra/components'
import BadgeGroup, { UniverTypes } from '@/components/BadgeGroup'

# 扩展 UI

<BadgeGroup values={[UniverTypes.GENERAL]} value={UniverTypes.GENERAL} />

## 新增菜单项

在 Univer 中，无论是顶部的工具栏菜单还是右键菜单，都可以通过编写插件来实现扩展。下面将介绍如何使用依赖注入系统中的 `IMenuService` 来注册菜单项。

### 1. 插件环境

确保你对[插件机制](/guides/sheet/architecture/univer)有所了解，并且已经通过 [@univerjs/create-cli](https://www.npmjs.com/package/@univerjs/create-cli) 新建了一个插件。

首先构造一个 `controller` 类，用于注册菜单项命令、菜单项图标、菜单项配置。
```ts
// src/plugin/controllers/custom-menu.controller.ts
@OnLifecycle(LifecycleStages.Steady, CustomMenuController)
export class CustomMenuController extends Disposable {
  constructor(
    @Inject(Injector) private readonly _injector: Injector,
    @ICommandService private readonly _commandService: ICommandService,
    @IMenuService private readonly _menuService: IMenuService,
    @Inject(ComponentManager) private readonly _componentManager: ComponentManager,
  ) {
    super();

    this._initCommands();
    this._registerComponents();
    this._initMenus();
  }

  /**
   * register commands
  */
  private _initCommands(): void { }

  /**
   * register icon components
  */
  private _registerComponents(): void { }

  /**
   * register menu items
  */
  private _initMenus(): void { }
}
```

将这个 `controller` 注册到插件中
```ts
// src/plugin/plugin.ts
const SHEET_CUSTOM_MENU_PLUGIN = 'SHEET_CUSTOM_MENU_PLUGIN';

export class UniverSheetsCustomMenuPlugin extends Plugin {
  static override type = UniverInstanceType.UNIVER_SHEET;
  static override pluginName = SHEET_CUSTOM_MENU_PLUGIN;

  constructor(
    @Inject(Injector) protected readonly _injector: Injector
  ) {
    super();
  }

  override onStarting(injector: Injector): void {
    ([
      [CustomMenuController],
    ] as Dependency[]).forEach((d) => injector.add(d));
  }
}
```

### 2. 菜单项命令

注册菜单之前，需要构造一个 `Command`，这个 `Command` 会在菜单被点击时执行。
```ts
// src/plugin/commands/operations/single-button.operation.ts
export const SingleButtonOperation: ICommand = {
  id: 'custom-menu.operation.single-button',
  type: CommandType.OPERATION,
  handler: async (accessor: IAccessor) => {
    alert('Single button operation');
    return true;
  },
};
```

将这个 `Command` 注册到 `ICommandService`
```ts
// src/plugin/controllers/custom-menu.controller.ts
private _initCommands(): void {
  [
    SingleButtonOperation
  ].forEach((c) => {
    this.disposeWithMe(this._commandService.registerCommand(c));
  });
}
```

### 3. 菜单项图标

如果你的菜单项需要图标，也需要提前注册图标。

先构造一个图标 tsx 组件
```ts
// src/plugin/components/button-icon/ButtonIcon.tsx
export function ButtonIcon() {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
      <path fill="currentColor" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m.16 14a6.981 6.981 0 0 0-5.147 2.256A7.966 7.966 0 0 0 12 20a7.97 7.97 0 0 0 5.167-1.892A6.979 6.979 0 0 0 12.16 16M12 4a8 8 0 0 0-6.384 12.821A8.975 8.975 0 0 1 12.16 14a8.972 8.972 0 0 1 6.362 2.634A8 8 0 0 0 12 4m0 1a4 4 0 1 1 0 8a4 4 0 0 1 0-8m0 2a2 2 0 1 0 0 4a2 2 0 0 0 0-4" />
    </svg>
  );
};
```

将这个图标注册到 `ComponentManager`
```ts
// src/plugin/controllers/custom-menu.controller.ts
private _registerComponents(): void {
  this.disposeWithMe(this._componentManager.register("ButtonIcon", ButtonIcon));
}
```

### 4. 菜单项国际化

如果你的菜单项需要国际化，需要提前添加好国际化资源。
```ts
// src/plugin/locale/zh-CN.ts
export default {
  customMenu: {
    button: '按钮',
    singleButton: '单个按钮',
  },
};
```
```ts
// src/plugin/locale/en-US.ts
export default {
  customMenu: {
    button: 'Button',
    singleButton: 'Single button',
  },
};
```

将这个国际化资源注册到 `ILocaleService`
```ts
// src/plugin/plugin.ts
export class UniverSheetsCustomMenuPlugin extends Plugin {
  static override type = UniverInstanceType.UNIVER_SHEET;
  static override pluginName = SHEET_CUSTOM_MENU_PLUGIN;

  constructor(
    @Inject(Injector) protected readonly _injector: Injector,
    @Inject(LocaleService) private readonly _localeService: LocaleService
  ) {
    super();

    this._localeService.load({
      zhCN,
      enUS
    });
  }
}
```

### 5. 菜单项配置

整理菜单项所需要的数据，包括菜单项的 id、类型、图标、标题、位置等。
```ts
export function CustomMenuItemSingleButtonFactory(): IMenuButtonItem<string> {
  return {
    // 绑定 Command id，单击该按钮将触发该命令
    id: SingleButtonOperation.id,
    // 菜单项的类型，在本例中，它是一个按钮
    type: MenuItemType.BUTTON,
    // 按钮的图标，需要在 ComponentManager 中注册
    icon: 'ButtonIcon',
    // 按钮的提示，优先匹配国际化，如果没有匹配到，将显示原始字符串
    tooltip: 'customMenu.singleButton',
    // 按钮的标题，优先匹配国际化，如果没有匹配到，将显示原始字符串
    title: 'customMenu.button',
    // 按钮位置，可以使用 MenuPosition 配置在工具栏或右键菜单，如果是表格，还可以使用 SheetMenuPosition 配置在行标题、列标题、工作表右键菜单
    positions: [MenuPosition.TOOLBAR_START, MenuPosition.CONTEXT_MENU],
  };
}
```

将这个菜单项注册到 `IMenuService`
```ts
// src/plugin/controllers/custom-menu.controller.ts
private _initMenus(): void {
  (
    [
      CustomMenuItemSingleButtonFactory
    ] as IMenuItemFactory[]
  ).forEach((factory) => {
    this.disposeWithMe(this._menuService.addMenuItem(this._injector.invoke(factory), {}));
  });
}
```

### 6. 下拉列表

除了添加单个按钮的菜单项，还可以添加一个下拉菜单项，具体实现方式类似，只在构造菜单项配置时有所区别：
- 将菜单项配置返回类型 `IMenuButtonItem<string>` 替换为 `IMenuSelectorItem<string>`
- 将菜单项类型 `MenuItemType.BUTTON` 替换为 `MenuItemType.SUBITEMS`
- 下拉列表主按钮需要自定义一个 id，作为下拉列表的唯一标识，用于关联下拉列表的子菜单项，子菜单项的 `positions` 需要填写主按钮的 id

```ts
// src/plugin/controllers/menu/dropdown-list.menu.ts
const CUSTOM_MENU_DROPDOWN_LIST_OPERATION_ID = 'custom-menu.operation.dropdown-list';

export function CustomMenuItemDropdownListMainButtonFactory(): IMenuSelectorItem<string> {
  return {
    id: CUSTOM_MENU_DROPDOWN_LIST_OPERATION_ID,
    type: MenuItemType.SUBITEMS,
    icon: 'MainButtonIcon',
    tooltip: 'customMenu.dropdownList',
    title: 'customMenu.dropdown',
    positions: [MenuPosition.TOOLBAR_START, MenuPosition.CONTEXT_MENU],
  };
}

export function CustomMenuItemDropdownListFirstItemFactory(): IMenuButtonItem<string> {
  return {
    id: DropdownListFirstItemOperation.id,
    type: MenuItemType.BUTTON,
    title: 'customMenu.itemOne',
    icon: 'ItemIcon',
    positions: [CUSTOM_MENU_DROPDOWN_LIST_OPERATION_ID],
  };
}
```

更多细节请参考 [自定义菜单 Playground](/playground?title=Custom+Menu)。

## 定制菜单项（隐藏菜单项）

在 Univer 中定制或隐藏菜单项是一种常见需求，我们为所有包含了菜单项的插件提供了配置项。只要知道菜单项对应的命令 ID，你就可以非常轻松地通过配置项来实现这一需求。

例如，隐藏加粗的菜单项：

```typescript
import { UniverSheetsUIPlugin, SetRangeBoldCommand } from '@univerjs/sheets-ui';

univer.registerPlugin(UniverSheetsUIPlugin, {
  menu: {
    [SetRangeBoldCommand.id]: {
      hidden: true,
    },
  },
});
```

关于如何获取命令 ID，可以参考教程[如何查找命令 ID](/guides/sheet/tutorials/find-the-command-id)。
